﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Net.NetworkInformation;
using System.Timers;
using log4net;
using System.Threading;
using WHT_Auth.Properties;

namespace WHO_Auth_TNG_Win {
    public class Auth : ICertificatePolicy {
        public static readonly int TIMER_INTERVAL = 15000;
        public static readonly int TIMER_ERROR_INTERVAL = 30000;
        public static readonly int TIMER_CREDENTIALS_REJECTED_INTERVAL = 30*60*1000;
        public static readonly int TIMER_CHECK_KILLSWITCH_INTERVAL = 30*60*1000;

        public static readonly string authaddress = "https://stuwe.net.uni-tuebingen.de/netaccess/connstatus.html";

        public string password = "";
        private System.Timers.Timer authTimer = new System.Timers.Timer();
        public AuthInfo authInfo = null;
        private static readonly ILog log = LogManager.GetLogger("WHO-Auth TNG");
        private AuthCode state = AuthCode.DEACTIVATED;
        private MainWindow mainWindow;
        private bool connectionCheckFailed = false;
        private Thread initThread;
        private Thread loginThread;
        private Thread retrieveThread;
        public bool enabled = true;
        

        public Auth(MainWindow mainWindow) {
            authTimer.Interval = TIMER_INTERVAL;
            authTimer.Elapsed += new ElapsedEventHandler(this.timerEvent);
            this.mainWindow = mainWindow;
            
        }

        public void initialize() {
            log.Debug("Initializing Auth");

            //Deactivate cert check
            System.Net.ServicePointManager.ServerCertificateValidationCallback +=
                delegate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors sslError)
                    {
                        bool validationResult = true;
                        return validationResult;
                    };

            initThread = new Thread(new ThreadStart(initializeThread));
            initThread.Start();
        }


        /// <summary>
        /// Called when program starts (when initialize() is called)
        /// </summary>
        private void initializeThread() {
            if (!password.Equals("") && enabled) {
                //Program is started. Check if connected. If yes, retrieve connection Info, otherwise login
                log.Info("Checking connection..."); //For logview
                checkConnection();
                if (getState() == AuthCode.NOT_CONNECTED) {
                    startLoginProcess();
                } else if (getState() == AuthCode.CONNECTED) {
                    retrieveThread = new Thread(retrieveAndPropagateConnectionInfo);
                    retrieveThread.Start();
                }
            }
        }

        public void startTimer() {
            try {
                authTimer.Start();
            } catch (NullReferenceException) {
                log.Error("Error enabling timer");
            }
        }

        public void stopTimer() {
            try {
                if (authTimer.Enabled) {
                    log.Info("Timer stopped");
                }
                authTimer.Stop();
            } catch (NullReferenceException) {
                log.Error("Error disabling timer");
            }
        }

        public bool timerRunning() {
            return authTimer.Enabled;
        }

        public void stopAll() {
            log.Info("Stopping all threads");
            if (loginThread != null && loginThread.IsAlive)
                loginThread.Abort();
            if (initThread != null && initThread.IsAlive)
                initThread.Abort();
            if (retrieveThread != null && retrieveThread.IsAlive)
                retrieveThread.Abort();
            authTimer.Stop();
        }

        public void timerEvent(Object sender, EventArgs e) {

                    //If enabled by user and killswitch not active
                    if (enabled && mayCheckConnection()) {
                        checkConnection();
                        if (getState() == AuthCode.NOT_CONNECTED) {
                            login(Settings.Default.username, password);
                        }
                    }
               
        }

        #region state handling

        public delegate void AuthStateChangeHandler(AuthCode newAuthCode);

        public event AuthStateChangeHandler AuthStateChange;

        public bool isConnected() {
            return (state == AuthCode.CONNECTED
                    || state == AuthCode.RETRIEVING_INFO
                    || state == AuthCode.RETRIEVAL_OK
                    || state == AuthCode.RETRIEVAL_FAILED
                    || state == AuthCode.LOGGED_IN);
        }

        private bool mayCheckConnection() {
            return (state != AuthCode.CHECKING_CONNECTION
                    && state != AuthCode.DEACTIVATED
                    && state != AuthCode.LOGGED_OUT
                    && state != AuthCode.LOGGING_IN
                    && state != AuthCode.KILLED);
        }

        private void setState(AuthCode newState) {
            state = newState;
            if (AuthStateChange != null)
                AuthStateChange(newState);
            switch (newState) {
                case AuthCode.CONNECTED:
                    if (connectionCheckFailed) {
                        //Computer is now connected but connection could not be checked before
                        //Retrieve new connection info as it was not loaded before in case there
                        //was no connection when the program was started
                        retrieveAndPropagateConnectionInfo();
                        connectionCheckFailed = false;
                        log.Info("Connection check now succeeded - Computer is connected");
                    } else {
                        log.Debug("Connected");
                    }
                    break;
                case AuthCode.RETRIEVAL_OK:
                    log.Info("Information retrieval succeeded");
                    break;
                case AuthCode.CHECKING_CONNECTION:
                    log.Debug("Checking connection");
                    break;
                case AuthCode.CONNECTION_UNKNOWN:
                    log.Warn("Connection status unknown");
                    break;
                case AuthCode.DEACTIVATED:
                    log.Info("Program deactivated");
                    stopTimer();
                    break;
                case AuthCode.ERROR:
                    log.Error("Error occured");
                    authTimer.Interval = TIMER_ERROR_INTERVAL;
                    break;
                case AuthCode.LOGGED_IN:
                    log.Info("Successfully logged in");
                    break;
                case AuthCode.LOGGED_OUT:
                    log.Info("Successfully logged out");
                    stopTimer();
                    break;
                case AuthCode.LOGGING_IN:
                    log.Info("Logging in");
                    break;
                case AuthCode.LOGIN_FAILED:
                    log.Error("Login failed");
                    authTimer.Interval = TIMER_ERROR_INTERVAL;
                    break;
                case AuthCode.LOGIN_REJECTED:
                    log.Error("Credentials rejected");
                    authTimer.Interval = TIMER_CREDENTIALS_REJECTED_INTERVAL;
                    break;
                case AuthCode.LOGIN_UNKNOWN:
                    log.Warn("Connection status unknown");
                    authTimer.Interval = TIMER_ERROR_INTERVAL;
                    break;
                case AuthCode.NOT_CONNECTED:
                    log.Info("Not connected");
                    break;
                case AuthCode.RETRIEVAL_FAILED:
                    log.Warn("Connection info could not be retrieved");
                    break;
                case AuthCode.RETRIEVING_INFO:
                    log.Info("Retrieving connection info");
                    break;
                case AuthCode.KILLED:
                    log.Fatal("Program was remotely disabled");
                    break;
                case AuthCode.CONNECTION_CHECK_FAILED:
                    if (!connectionCheckFailed) {
                        connectionCheckFailed = true;
                        log.Error("Error checking connection. Please wait...");
                    }
                    break;
            }

            //If no error occured set timer back to old interval
            if (newState != AuthCode.ERROR
                && newState != AuthCode.LOGIN_FAILED
                && newState != AuthCode.CONNECTION_CHECK_FAILED
                && newState != AuthCode.CHECKING_CONNECTION
                && newState != AuthCode.LOGIN_REJECTED
                && newState != AuthCode.KILLED
                )
                authTimer.Interval = TIMER_INTERVAL;

            //Assure that timer is running if it should
            if (newState != AuthCode.DEACTIVATED && newState != AuthCode.LOGGED_OUT && newState != AuthCode.KILLED)
                startTimer();
            if (newState != AuthCode.CONNECTION_CHECK_FAILED && newState != AuthCode.CHECKING_CONNECTION)
                connectionCheckFailed = false;
        }

        private AuthCode getState() {
            return state;
        }

        #endregion



        /// <summary>
        /// Checks if the user is connected
        /// </summary>
        private void checkConnection() {
            setState(AuthCode.CHECKING_CONNECTION);
            
            try {
                log.Debug("Checking " + authaddress);

                //Open connectionstatus.html
                HttpWebRequest request = (HttpWebRequest) WebRequest.Create(authaddress);
                request.KeepAlive = false;
                HttpWebResponse httpWebResponse = (HttpWebResponse) request.GetResponse();
                
                if (httpWebResponse.StatusCode == HttpStatusCode.OK) {
                    StreamReader reader = new StreamReader(httpWebResponse.GetResponseStream());
                    string htmlresponse = reader.ReadToEnd();

                    if (htmlresponse.Contains("You are logged in")) {
                        log.Debug("User is logged in");
                        setState(AuthCode.CONNECTED);
                    } else if (htmlresponse.Contains("You are not logged in")) {
                        log.Debug("User is not logged in");
                        setState(AuthCode.NOT_CONNECTED);
                    }
                    reader.Close();
                } else {
                    log.Debug("User state unknown");
                    setState(AuthCode.CONNECTION_CHECK_FAILED);
                }
                
                httpWebResponse.Close();

            } catch (Exception e) {
                //log.Error("Error checking connection123:", e);
                setState(AuthCode.CONNECTION_CHECK_FAILED);
            }
        }

        #region login und logout

        /// <summary>
        /// Check if login is necessary and do so if necessary
        /// </summary>
        public void attemptLogin() {
            checkConnection();
            if (!isConnected()) {
                doLogin();
            } else {
                log.Info("No login necessary - already connected");
            }
        }

        /// <summary>
        /// Start logging in with the saved username and password
        /// </summary>
        private void startLoginProcess() {
            login(WHT_Auth.Properties.Settings.Default.username, password);
        }

        public bool CheckValidationResult(ServicePoint sp,
                                          X509Certificate certificate, WebRequest request, int error) {
            return true;
        }

        /// <summary>
        /// Tries to login when logged out and vice versa
        /// </summary>
        public void loginLogout() {
            if (isConnected()) {
                doLogout();
            } else {
                doLogin();
            }
        }

        /// <summary>
        /// Starts the login process
        /// </summary>
        public void doLogin() {
            loginThread = new Thread(new ThreadStart(startLoginProcess));
            loginThread.Start();
        }


        /// <summary>
        /// Sends a logout to command to the gateway
        /// </summary>
        public void doLogout() {
            stopTimer();
            WebClient client = new WebClient();
            string sret = "";
            try {
                //GET login page (otherwise the next time it will return "session expired"
                client.DownloadData("https://stuwe.net.uni-tuebingen.de/netaccess/connstatus.html");
                //POST user data
                byte[] userData = System.Text.Encoding.ASCII.GetBytes("logout=true");
                byte[] bret = client.UploadData("https://stuwe.net.uni-tuebingen.de/netaccess/connstatus.html", "POST", userData);
                sret = System.Text.Encoding.ASCII.GetString(bret);
            } catch (WebException we) {
                log.Error(we.Message + "\n" + we.Status.ToString());
            } catch (NotSupportedException ne) {
                log.Error(ne.Message + "\n");
            }
            if (sret.Contains("are not logged in")) {
                setState(AuthCode.LOGGED_OUT);
            }
        }

        /// <summary>
        /// Sends the login information to the gateway
        /// </summary>
        /// <param name="username"></param>
        /// <param name="password"></param>
        public void login(string username, string password) {
            setState(AuthCode.LOGGING_IN);
            WebClient client = new WebClient();
            string sret;
            try {
                //GET login page (otherwise the next time it will return "session expired"
                client.DownloadData("https://stuwe.net.uni-tuebingen.de/netaccess/loginuser.html");
                //POST user data
                byte[] userData = System.Text.Encoding.ASCII.GetBytes("username=" + username + "&password=" + password);
                byte[] bret = client.UploadData("https://stuwe.net.uni-tuebingen.de/netaccess/loginuser.html", "POST", userData);
                sret = System.Text.Encoding.ASCII.GetString(bret);
                if (sret.Contains("are logged in")) {
                    if (parseConnectionInfo(sret)) {
                        setState(AuthCode.RETRIEVAL_OK);
                    } else {
                        setState(AuthCode.RETRIEVAL_FAILED);
                    }
                } else if (sret.Contains("Rejected")) {
                    setState(AuthCode.LOGIN_REJECTED);
                } else {
                    //Not sure what happened. See if computer is connected
                    log.Info("Login success unknown... Checking connection");
                    checkConnection();
                }
            } catch (WebException we) {
                log.Error(we.Message + "\n" + we.Status.ToString(), we);
                setState(AuthCode.LOGIN_FAILED);
            } catch (NotSupportedException ne) {
                log.Error(ne.Message + "\n", ne);
                setState(AuthCode.LOGIN_FAILED);
            }
        }

        #endregion

        #region connection info stuf

        //Is called only when program is started and user is connected
        //(in other cases connection info is part of the POST response)
        /// <summary>
        /// Downloads https://stuwe.net.uni-tuebingen.de/netaccess/connstatus.html, parses the HTML code and then
        /// fires a state change
        /// </summary>
        private void retrieveAndPropagateConnectionInfo() {
            setState(AuthCode.RETRIEVING_INFO);
            WebClient client = new WebClient();
            try {
                //GET login page (otherwise the next time it will return "session expired"
                byte[] bret = client.DownloadData("https://stuwe.net.uni-tuebingen.de/netaccess/connstatus.html");
                string sret = System.Text.Encoding.ASCII.GetString(bret);
                if (parseConnectionInfo(sret)) {
                    setState(AuthCode.RETRIEVAL_OK);
                    connectionCheckFailed = false;
                } else {
                    setState(AuthCode.RETRIEVAL_FAILED);
                }
            } catch (WebException we) {
                log.Error(we.Message + "\n" + we.Status.ToString());
                setState(AuthCode.RETRIEVAL_FAILED);
            } catch (NotSupportedException ne) {
                log.Error(ne.Message);
                setState(AuthCode.RETRIEVAL_FAILED);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="htmlCode">The HTML code to parse</param>
        /// <returns>true if the parsing was successful</returns>
        private bool parseConnectionInfo(string htmlCode) {
            try {
                string ip = "";
                int ipStart = htmlCode.IndexOf("User IP:") + 9;
                if (ipStart > 0) {
                    int ipEnd = htmlCode.IndexOf("<br>", ipStart);
                    if (ipEnd > ipStart) {
                        ip = htmlCode.Substring(ipStart, ipEnd - ipStart);
                    }
                }

                string connectedSince = "";
                int connStart = htmlCode.IndexOf("Since:") + 7;
                if (connStart > 0) {
                    int connEnd = htmlCode.IndexOf("<br>", connStart);
                    if (connEnd > connStart) {
                        connectedSince = htmlCode.Substring(connStart, connEnd - connStart);
                    }
                }

                string connectedFor = "";
                int connForStart = htmlCode.IndexOf("for Hours:") + 4;
                if (connForStart > 0) {
                    int connForEnd = htmlCode.IndexOf(")", connForStart);
                    if (connForEnd > connForStart) {
                        connectedFor = htmlCode.Substring(connForStart, connForEnd - connForStart);
                    }
                }
                authInfo = new AuthInfo(connectedSince, ip, connectedFor);
                return true;
            } catch (Exception) {
                return false;
            }
        }

        #endregion
    }
}